package org.ykh.oos.service;

import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.*;
import com.aliyuncs.exceptions.ClientException;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;

import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.web.client.RestTemplate;

import org.ykh.oos.config.property.OssProperty;
import org.ykh.oos.pojo.dto.OssCallbackDto;


/**
 * @author ykh
 */
@Service
@Slf4j
public class OssService {


    private OssProperty ossProperty;

    public OssService(OssProperty ossProperty, RestTemplate restTemplate) {
        this.ossProperty = ossProperty;
        this.restTemplate = restTemplate;
    }

    public OSS createOssClientV1() {
        // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1（杭州）为例，Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
//        String endpoint = "yourEndpoint";

//        // 从环境变量中获取访问凭证。运行本代码示例之前，请先配置环境变量。
//        EnvironmentVariableCredentialsProvider credentialsProvider =
//                CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
//        // 创建OSSClient实例。
//        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
        return new OSSClientBuilder().build(ossProperty.getEndpoint(), ossProperty.getAccessId(),
                ossProperty.getAccessKey());
    }

    /**
     * @return
     * @throws ClientException
     */
    public OSS createOssClientV4() throws ClientException {
        // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1（杭州）为例，Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
//        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 填写Endpoint对应的Region信息，例如cn-hangzhou。
//        String region = "cn-hangzhou";

        // 从环境变量中获取访问凭证。运行本代码示例之前，请先配置环境变量。
//        EnvironmentVariableCredentialsProvider credentialsProvider =
//                CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        final CredentialsProvider credentialsProvider = new CredentialsProvider() {
            @Override
            public void setCredentials(Credentials creds) {
                throw new RuntimeException("非法操作！！！");
            }

            @Override
            public Credentials getCredentials() {
                return new DefaultCredentials(ossProperty.getAccessId(),
                        ossProperty.getAccessKey());
            }
        };

        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        return OSSClientBuilder.create()
                .endpoint(ossProperty.getEndpoint())
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(ossProperty.getRegion())
                .build();
    }


    /**
     * 服务端签名直传
     * <p>
     * 生成用于PostObject请求的策略及相关信息。
     *
     * @param ossClient OSS客户端，用于执行与OSS的交互操作。
     * @param dir       上传文件的目录前缀。
     * @return 包含PostObject请求所需信息的Map对象，如访问ID、编码后的策略、签名等。作为前端直传的凭证
     */
    public Map<String, String> policy(OSS ossClient, String dir) {
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            // PostObject请求最大可支持的文件大小为5 GB，即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            // 生成上传策略
            // 设置上传文件的条件，最大支持上传1 GB的文件
            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);

            byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);

            String postSignature = ossClient.calculatePostSignature(postPolicy);


            // 准备返回给调用者的Map，包含访问ID、编码后的策略、签名等信息
            Map<String, String> respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", ossProperty.getAccessId());
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            //oss回调应用服务器地址
            respMap.put("host", ossProperty.getHost());
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));
            return respMap;
        }finally {
            ossClient.shutdown();
        }
    }

    /**
     * https://help.aliyun.com/zh/oss/use-cases/overview-20?spm=a2c4g.11186623.0.i73#concept-qp2-g4y-5db
     * 服务端签名直传,并设置回调
     */

    public Map<String, String> policyCallback(OSS ossClient, String dir) {
        try {
            final Map<String, String> respMap = policy(ossClient, dir);
            //设置回调
            JSONObject jasonCallback = new JSONObject();
            //构造回调参数，Callback参数是由一段经过Base64编码的JSON字符串（字段）构成的。
            //文件上传成功后，OSS向此URL发送回调请求。
            jasonCallback.put("callbackUrl", ossProperty.getCallback());
            //发起回调时请求Body的值
            jasonCallback.put("callbackBody", OssCallbackDto.forUrlParam()
            );
            //发送回调请求类型也支持application/json
            jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
            //编码回调json信息
            String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
            respMap.put("callback", base64CallbackBody);
            return respMap;
        }finally {
            ossClient.shutdown();
        }
    }



    /**
     * 验证回调请求是否由OSS发起
     *
     * @param request
     * @return
     * @throws NumberFormatException
     * @throws IOException
     */
    public boolean VerifyOSSCallbackRequest(HttpServletRequest request, Integer contentLength,
                                            String authorizationInput,
                                            String pubKeyInput)
            throws NumberFormatException, IOException, URISyntaxException {
        boolean ret;
        //验证签名的方式
        //Result = rsa_verify(public_key, md5(url_decode(path) + query_string + ‘\n’ + body), base64_decode(authorization))
        //其中，public_key为公钥，authorization为回调头中的签名。

        //验证签名的过程
        //a.回调请求的x-oss-pub-key-url头保存的是公钥URL地址的base64编码，需要对x-oss-pub-key-url执行base64解码后获取公钥。
        //public_key = urlopen(base64_decode(x-oss-pub-key-url头的值))
        byte[] pubKey = BinaryUtil.fromBase64String(pubKeyInput);
        String pubKeyAddr = new String(pubKey);
        //根据地址获取公钥
        String retString = executeGet(pubKeyAddr);
        log.info("oss响应公钥：{}",request);
        retString = retString.replace("-----BEGIN PUBLIC KEY-----", "");
        retString = retString.replace("-----END PUBLIC KEY-----", "");
        //注意：OSS颁发的public_key中x-oss-pub-key-url头的值必须以http://gosspublic.alicdn.com/或者https://gosspublic.alicdn.com/开头。
        if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/")
                && !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/")) {
            log.warn("pub key addr must be oss address");
            return false;
        }

        //b.获取base64解码后的签名。
        byte[] authorization = BinaryUtil.fromBase64String(authorizationInput);

        //c.获取待签名字符串，方法与签名一致。
        //sign_str = url_decode(path) + query_string + ‘\n’ + body
        String ossCallbackBody = GetPostBody(request.getInputStream(), contentLength);
        String queryString = request.getQueryString();
        String uri = request.getRequestURI();
        String authStr = java.net.URLDecoder.decode(uri, "UTF-8");
        if (queryString != null && !"".equals(queryString)) {
            authStr += "?" + queryString;
        }
        authStr += "\n" + ossCallbackBody;

        //d.验证签名。
        ret = doCheck(authStr, authorization, retString);
        return ret;
    }


    private String GetPostBody(InputStream is, int contentLen) {
        if (contentLen > 0) {
            int readLen = 0;
            int readLengthThisTime = 0;
            byte[] message = new byte[contentLen];
            try {
                while (readLen != contentLen) {
                    readLengthThisTime = is.read(message, readLen, contentLen - readLen);
                    if (readLengthThisTime == -1) {// Should not happen.
                        break;
                    }
                    readLen += readLengthThisTime;
                }
                return new String(message);
            } catch (IOException e) {
            }
        }
        return "";
    }

    private final RestTemplate restTemplate;

    /**
     * 获取public key
     *
     * @param url
     * @return
     */
    private String executeGet(String url) throws URISyntaxException {
        return restTemplate.execute(new URI(url), HttpMethod.GET, request -> {
            log.info("向oss-url：{}发起请求获取公钥",url);
        }, (clientHttpResponse) -> {
            StringBuilder sb = new StringBuilder("");
            try (
                    BufferedReader in = new BufferedReader(
                            new InputStreamReader(clientHttpResponse.getBody())
                    )
            ) {
                String line = "";
                String NL = System.getProperty("line.separator");//获取系统换行符
                while ((line = in.readLine()) != null) {
                    sb.append(line).append(NL);
                }
            }
            return sb.toString();
        });
    }


    /**
     * 使用RSA算法验证数据的签名是否有效。
     * Result = rsa_verify(public_key, md5(url_decode(path) + query_string + ‘\n’ + body), base64_decode(authorization))
     * @param content 待验证的数据内容。base64_decode(authorization)
     * @param sign 数据的签名，用于验证数据的真实性和完整性。 md5(url_decode(path) + query_string + ‘\n’ + body)
     * @param publicKey 公钥，用于验证签名。以Base64编码格式提供。public_key
     * @return 验证结果，如果签名有效返回true，否则返回false。
     */
    public static boolean doCheck(String content, byte[] sign, String publicKey) {
        try {
            // 将公钥从Base64编码转换为字节数组
            byte[] encodedKey = BinaryUtil.fromBase64String(publicKey);
            // 实例化RSA的KeyFactory
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            // 通过公钥字节数组生成PublicKey对象
            PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
            // 实例化签名对象，并使用RSA算法和公钥进行初始化，用于验证签名
            Signature signature = Signature.getInstance("MD5withRSA");
            signature.initVerify(pubKey);
            // 更新签名对象，使之包含待验证数据的内容
            signature.update(content.getBytes());
            // 验证签名是否有效，并返回验证结果
            return signature.verify(sign);
        } catch (Exception e) {
            // 捕获验证过程中可能抛出的异常，并打印异常栈信息
            e.printStackTrace();
        }
        // 如果发生异常，或验证失败，则返回false
        return false;
    }


    /**
     * 创建一个OSS存储桶。
     *
     * @param bucketName 想要创建的存储桶的名称。
     * @throws ClientException 如果在创建存储桶的过程中出现任何客户端错误。
     */
    public void createBucket(String bucketName) throws ClientException {
        final OSS ossClient = createOssClientV1();
        try {
            ossClient.createBucket(bucketName);
        }finally {
            ossClient.shutdown();
        }
    }



    /**
     * 上传文件到OSS（阿里云对象存储）。
     *
     * @param bucketName  存储桶的名称，指定文件将上传到哪个存储桶中。
     * @param objectName  上传文件在存储桶中的对象名称（路径）。
     * @param inputStream 要上传的文件的输入流。
     * @throws ClientException 如果上传过程中出现任何客户端错误。
     */
    public String upload(String bucketName, String objectName, InputStream inputStream) throws ClientException {
        final OSS ossClient = createOssClientV1();
        try {
            ossClient.putObject(bucketName == null ? ossProperty.getBucket() : bucketName, objectName, inputStream);
            // <http或者https>://<Bucket>.<Endpoint>/<Object>，例如https://examplebucket.oss-cn-hangzhou.aliyuncs.com/example.txt。
            return ossProperty.getHost()+"/"+objectName;
        }finally {
            ossClient.shutdown();
        }
    }


    /**
     * 获取
     * @param bucketName
     * @param objectName
     * @throws ClientException
     * @throws IOException
     */
    public void getObject(String bucketName, String objectName) throws ClientException, IOException {
        final OSS ossClient = createOssClientV1();
        try {
            // 调用ossClient.getObject返回一个OSSObject实例，该实例包含文件内容及文件元数据。
            OSSObject ossObject = ossClient.getObject(bucketName, objectName);
            // 调用ossObject.getObjectContent获取文件输入流，可读取此输入流获取其内容。
            InputStream content = ossObject.getObjectContent();
            if (content != null) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(content));
                while (true) {
                    String line = reader.readLine();
                    if (line == null) {
                        break;
                    }
                    System.out.println("\n" + line);
                }
                // 数据读取完成后，获取的流必须关闭，否则会造成连接泄漏，导致请求无连接可用，程序无法正常工作。
                content.close();
            }
        }finally {
            ossClient.shutdown();
        }
    }




        /**
         * 获取所有
         * @param bucketName
         * @throws ClientException
         */
    public void listObject(String bucketName) throws ClientException {
        final OSS ossClient = createOssClientV1();
        try {
            // ossClient.listObjects返回ObjectListing实例，包含此次listObject请求的返回结果。
            ObjectListing objectListing = ossClient.listObjects(bucketName);
            // objectListing.getObjectSummaries获取所有文件的描述信息。
            for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) {
                System.out.println(" - " + objectSummary.getKey() + "  " +
                        "(size = " + objectSummary.getSize() + ")");
            }
        }finally {
            ossClient.shutdown();
        }
    }



    /**
     * 删除
     * @param bucketName
     * @param objectName
     * @throws ClientException
     */
    public void deleteObject(String bucketName, String objectName) throws ClientException {
        final OSS ossClient = createOssClientV1();
        try {
            ossClient.deleteObject(bucketName, objectName);
        }finally {
            ossClient.shutdown();
        }
    }


}
